/* Copyright 2009 Anthony Surma
This file is part of Java's Kademlia Extension (JKADX).

JKADX is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.

JKADX is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with JKADX.  If not, see <http://www.gnu.org/licenses/>.
 */
package jkadx.dht;

import java.util.Arrays;
import java.util.Random;

public final class UniqueId
{

    private   final static char   ONE_CHAR     = '1';
    private   final static char   ZERO_CHAR    = '0';
    private   final static char[] CHAR_ARRAY   = UniqueId.initCharArray();
              final static Random RAND         = new Random();
              final static int    TYPE_SIZE    = Integer.SIZE;
    public    final static int    NBITS        = 160; // number of bits
              final static int    LENGTH       = NBITS / TYPE_SIZE;
              final static int    TYPE_SIZE_M1 = TYPE_SIZE - 1; // for convenience
              final static int    LENGTH_M1    = LENGTH - 1; // for convenience
              final        int[]  bits;        // be careful to not break immutability
              final        int    hashCode;

    public UniqueId(final String newId)
    {
	int[] bits = stringToIntArray(newId);
	this.bits = bits;
	this.hashCode = (bits[LENGTH_M1]);
    }

    public UniqueId (final byte[] bytes, int offset)
    {
        final int[] bits = new int[LENGTH];

        for (int i = 0; i < LENGTH; i++)
        {
            bits[i] = ((bytes[offset++] & 0xFF) << 24)
                    + ((bytes[offset++] & 0xFF) << 16)
                    + ((bytes[offset++] & 0xFF) <<  8)
                    +  (bytes[offset++] & 0xFF);
        }

	this.bits = bits;
	this.hashCode = (bits[LENGTH_M1]);
    }
    
    UniqueId(final int[] newId, boolean unsafe)
    {
	this.bits = newId;
	this.hashCode = (newId[LENGTH_M1]);
    }

    @Testing UniqueId()
    {
	this.bits = new int[LENGTH]; // all zeros
	this.hashCode = 0;
    }

    @Testing UniqueId(final int[] newId)
    {
	final int[] bits = new int[LENGTH];

	System.arraycopy(newId, 0, bits, 0, LENGTH);
	this.bits = bits;
	this.hashCode = (bits[LENGTH_M1]);
    }

    @Override
    public String toString()
    {
        final char[] result = CHAR_ARRAY.clone();
	final int[] bits = this.bits;

	int j = TYPE_SIZE_M1;
	for (int index = 0; index < LENGTH; index++)
	{
	    for (int x = bits[index]; j >= 0; --j)
	    {
		if (((x >>> j) & 1) == 1) // 4 bit switch would be faster
		{
		    result[(index << LENGTH) + (TYPE_SIZE_M1 - j)] = ONE_CHAR; 
		}
	    }
	    j = TYPE_SIZE_M1;
	}
	return new String(result);
    }

    @Override
    public int hashCode()
    {	
	return hashCode;
    }

    @Override
    public boolean equals(final Object obj)
    {
        int[] thatBits = ((UniqueId)obj).bits;
        
	return (this.bits[4] == thatBits[4] && this.bits[3] == thatBits[3] &&
                this.bits[2] == thatBits[2] && this.bits[1] == thatBits[1] &&
                this.bits[0] == thatBits[0] );
    }

    public boolean matches(final UniqueId that)
    {
	return (this.bits[4] == that.bits[4] && this.bits[3] == that.bits[3] &&
                this.bits[2] == that.bits[2] && this.bits[1] == that.bits[1] &&
                this.bits[0] == that.bits[0] );
    }
   
    public static int getMSBAfterXoring(final UniqueId first, final UniqueId second)
    {
        int x;

	for (int i = 0; i < LENGTH; ++i)
	{
	    x = first.bits[i] ^ second.bits[i];
            if (x != 0)
            {
                for (int j = TYPE_SIZE_M1; j >= 0; --j)
                {
                    if ((x >>> j) == 1)
                    {
                        return (i << LENGTH) + (TYPE_SIZE_M1 - j);
                    }
                }
            }
	}
	return -1;
    }

    public static UniqueId newRandom()
    {
	final int[] randomInts = new int[LENGTH];
        Random RAND = UniqueId.RAND;

	randomInts[0] = RAND.nextInt();
	randomInts[1] = RAND.nextInt();
	randomInts[2] = RAND.nextInt();
	randomInts[3] = RAND.nextInt();
	randomInts[4] = RAND.nextInt();

	return new UniqueId(randomInts, true);
    }

    public static int[] getRandomInts()
    {
	final int[] randomInts = new int[LENGTH];
        Random RAND = UniqueId.RAND;

	randomInts[0] = RAND.nextInt();
	randomInts[1] = RAND.nextInt();
	randomInts[2] = RAND.nextInt();
	randomInts[3] = RAND.nextInt();
	randomInts[4] = RAND.nextInt();

	return randomInts;
    }

    public void insert(byte[] payload, int offset)
    {
        int[] bits = this.bits;

        for (int i = 0; i < LENGTH; ++i)
        {
            payload[offset++] = (byte)(bits[i] >>> 24);
            payload[offset++] = (byte)(bits[i] >>> 16);
            payload[offset++] = (byte)(bits[i] >>> 8);
            payload[offset++] = (byte)(bits[i]);
        }
    }

    private static char[] initCharArray()
    {
        char[] chars = new char[NBITS];
        Arrays.fill(chars, UniqueId.ZERO_CHAR);
        return chars;
    }

    public int[] getBits()
    {
	return Arrays.copyOf(bits, LENGTH);
    }

    @Testing static int[] stringToIntArray(String stringOfBits)
    {
	int[]  result     = new int[LENGTH];
	int    difference = NBITS - stringOfBits.length();
	char[] c          = new char[NBITS];

	for (int i = 0; i < difference; ++i)
	{
	    c[i] = ZERO_CHAR;
	}
	for (int i = difference; i < NBITS; ++i)
	{
	    c[i] = stringOfBits.charAt(i - difference);
	}
	for (int i = 0; i < NBITS; i += TYPE_SIZE)
	{
	    if (c[i] == ONE_CHAR)
	    {
		c[i] = ZERO_CHAR;
		for (int j = i + 1; j < i + TYPE_SIZE; ++j)
		{
		    if (c[j] == ONE_CHAR)
		    {
			c[j] = ZERO_CHAR;
		    }
		    else
		    {
			c[j] = ONE_CHAR;
		    }
		}
		result[i >>> LENGTH] = -(Integer.parseInt(String.copyValueOf(c, i, TYPE_SIZE), 2) + 1);
	    }
	    else
	    {
		result[i >>> LENGTH] = Integer.parseInt(String.copyValueOf(c, i, TYPE_SIZE), 2);
	    }
	}
	return result;
    }

    public int compareTo(final UniqueId that, final UniqueId target)
    {
	// compares first and second to see if first is closer to target
	// than the second.
        int   index = 0;
        int   temp;
        int   mask;

        do
        {
            temp = this.bits[index] ^ target.bits[index];
            mask = that.bits[index] ^ target.bits[index] ^ temp;

            if (mask != 0) // // This is very likely to be true
            {
                mask |= (mask >>>  1);
                mask |= (mask >>>  2);
                mask |= (mask >>>  4);
                mask |= (mask >>>  8);
                mask |= (mask >>> 16);
                mask -= (mask >>>  1);
                return ((temp & mask) == mask) ? 1 : -1;
            }
            ++index;
         } while (index < UniqueId.LENGTH);
         return 0;
                       // returns -1 if this is closer to target
	               // returns +1 if that is closer to target
                       // returns  0 if this == that
    }

}